Day 5 購物車用 Hash 而不是 String,理由是 HINCRBY 改一個欄位不用讀寫整包。
但同樣一個 Hash、同樣只放一個欄位,有的 key 佔 64 bytes,有的佔 9360 bytes。存的東西一模一樣,差在 Redis 會依資料大小偷偷換底層結構,而且換過去就回不來。
今天只有一個指令。

同一個 key 只是把內容從 1 改成 100件,編碼就從 int 變成 embstr 了。
每個型別都有兩套底層結構:資料少的時候用省空間的那套,多了換成查得快的那套。
| 型別 | 小的時候 | 大了之後 |
|---|---|---|
| String | int(純數字)、embstr(44 字元以內) |
raw |
| Hash | listpack |
hashtable |
| List | listpack |
quicklist |
| Set | intset(全是數字)、listpack |
hashtable |
| ZSet | listpack |
skiplist |
這些名字不用特別記,只要記得「小的時候一種、大了換一種」就好。
門檻寫在設定檔裡,CONFIG GET 看得到:
CONFIG GET hash-max-listpack-entries # 512
CONFIG GET set-max-listpack-entries # 128
CONFIG GET set-max-intset-entries # 512
CONFIG GET zset-max-listpack-entries # 128
CONFIG GET hash-max-listpack-value # 64
前四個(entries)是「幾筆以內」,最後一個(value)是「單一個值幾字元以內」,超過任何一個就會轉換。
拿 Hash 實測,塞到第 512 個欄位還是 listpack,第 513 個就換掉
(這行在主機的終端機下,不是在 redis-cli 裡面):
for i in $(seq 1 512); do docker exec redis30days redis-cli HSET cart:1 p$i 1 > /dev/null; done
OBJECT ENCODING cart:1 # listpack,512 個欄位還撐得住
HSET cart:1 p513 1
OBJECT ENCODING cart:1 # hashtable,多一個就換掉了

值太長也一樣。只放一個欄位,值 64 字元還是 listpack,65 字元直接變 hashtable。
這是今天真正要記的事。把剛剛那個 Hash 的欄位刪回剩一個:
for i in $(seq 2 513); do docker exec redis30days redis-cli HDEL cart:1 p$i > /dev/null; done
HLEN cart:1 # 1
OBJECT ENCODING cart:1 # hashtable,沒有變回 listpack
MEMORY USAGE cart:1 # 9360
同樣只有一個欄位,另外建一個來對照:
HSET cart:2 p1 1
OBJECT ENCODING cart:2 # listpack
MEMORY USAGE cart:2 # 64

資料一模一樣,量出來差 146 倍。
9360 不全是編碼造成的。hashtable 的桶子是撐到 513 筆的時候配的,沒人碰這個 key 的話它就不會縮,HGET 讀一次之後會掉到 1168。但縮完了還是 1168 比 64,差 18 倍,而且 OBJECT ENCODING 永遠是 hashtable。
要省回來只有一條路:DEL 掉重建。
只有 List 是例外。 撐到 2000 筆變成 quicklist 之後,LTRIM 砍回十筆會自己變回 listpack,LPOP、LREM 也一樣。Hash、Set、ZSet 三個都回不去。
Set 全部是數字的時候用 intset,混進一個字串就掉到 listpack:
SADD enc:set 1 2 3 4 5 # intset
SADD enc:set tag # listpack
SREM enc:set tag # 還是 listpack

那個字串明明已經拿掉了,回不去 intset。跟上面是同一件事。
兩種結構可以這樣想:
listpack 像一張便條紙,一行一行往下寫,擠得很滿,所以省。缺點是要改中間那行得整張重抄,資料一多就慢。hashtable 像一本有索引的資料夾,每一筆配一個格子,查得快。但格子是事先開好的,只放一筆,空格子照樣佔位置。小資料用 hashtable,等於為了一張便條紙開一整本資料夾。
一個 key 差一千多 bytes 好像沒什麼,但這種 key 通常是「一個使用者一個」。上面量到的 64 對 1168,一萬個使用者就是 0.6MB 對 11.1MB,而且兩邊 HLEN 都是 1,從外面完全看不出來。
購物車、使用者標籤、Day 8 的延遲關單佇列都是這種「平常很小、偶爾被塞爆」的 key。解法只有兩個:程式裡限制上限(購物車最多幾件),或是定期把這種 key DEL 掉重建。
Day 4 說每個快取 key 都要設 TTL。但設了 TTL 的 key,時間到的那一秒真的被刪掉了嗎?明天繼續看 Redis 怎麼刪過期的 key,還有為什麼 DBSIZE 的數字會騙人![]()